﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Effects.Wave
{
    /// <summary>
    ///  Class for loading wave files from files.
    /// </summary>
    /// <remarks>
    ///  Only PCM Wave files  (no compression) are supported at the moment
    /// </remarks>
    public class WaveReader
    {
        #region input_file
        // path to the input file and reader over it

        /// <summary>
        ///  Path to the file.
        /// </summary>
        private string FileName;

        /// <summary>
        ///  Binary reader over the file at FileName
        /// </summary>
        private BinaryReader file;

        #endregion

        #region constructor
        // constructor

        /// <summary>
        ///  Creates WaveReader that reads from given file
        /// </summary>
        /// <param name="filename">Path to the file</param>
        public WaveReader(String filename)
        {
            this.FileName = filename;
        }

        #endregion

        #region load_method
        // load the input file

        /// <summary>
        ///  Loads file.
        /// </summary>
        /// <returns> Instance of Wave that represents wave file or null if some error occured during loading </returns>
        public Wave Load()
        {
            try
            {
                file = new BinaryReader(new FileStream(FileName, FileMode.Open, FileAccess.ReadWrite), Encoding.Unicode);
                // now try to read wave file
                // fisrt 4 bytes == RIFF
                byte[] buffer = new byte[4];
                file.Read(buffer, 0, 4);

                if (new ASCIIEncoding().GetString(buffer) != "RIFF")
                    throw new InvalidFileFormatException("No RIFF type file");

                // file length in bytes, 8 byte offset
                file.Read(buffer, 0, 4);
                if (!BitConverter.IsLittleEndian)
                    Array.Reverse(buffer);

                uint filelength = System.BitConverter.ToUInt32(buffer, 0);

                // riff type == WAVE
                file.Read(buffer, 0, 4);
                if (new ASCIIEncoding().GetString(buffer) != "WAVE")
                    throw new InvalidFileFormatException("No WAVE type");

                FmtChunk fmtChunk = null;
                DataChunk dataChunk = null;
                while (file.BaseStream.CanRead && dataChunk == null)
                {
                    // now fmt and data chunks
                    file.Read(buffer, 0, 4);
                    string tmp = "";
                    for (int i = 0; i < 4; i++)
                        tmp = tmp + (char)buffer[i];
                    //string tmp = BitConverter.ToString(buffer);

                    if (tmp == "fmt ")
                    {
                        // format chunk
                        fmtChunk = ReadFmtChunk();
                    }
                    else if (tmp == "data")
                    {
                        // datat chunk
                        dataChunk = ReadDataChunk((short) fmtChunk.BitsPerSample);
                    }
                    else
                    {
                        // other chunk - ignore
                        SkipChunk();
                    }
                }

                if (fmtChunk == null)
                    throw new InvalidFileFormatException("No fmt chunk found in file: " + FileName);
                if (dataChunk == null)
                    throw new InvalidFileFormatException("No data chunk found in file: " + FileName);

                Wave ret = new Wave(dataChunk, fmtChunk, filelength);
                return ret;

            }
            catch (FileNotFoundException e)
            {
                throw new IOException("File not found: " + e.FileName);
             }
            catch (DirectoryNotFoundException )
            {
                throw new IOException("Directory not found");
             }
            catch (IOException)
            {
                throw new IOException("Problem occured while reading the file: " + FileName);
             }
            catch (InvalidFileFormatException e)
            {
                throw new IOException("Choosen file has unsupported format: " + e.Message);
            }
            catch (Exception)
            {
                throw new IOException("An error occured while reading the file: " + FileName);
            }
            finally
            {
                if (file != null)
                    file.Close();
            }
        }

        /// <summary>
        ///  Occurs while performing Load method when WaveReader reports progress change.
        /// </summary>
        public event IoOperationReportProgressEventHandler ReportProgress;

        #endregion

        #region read_chunks
        // read format and dat chunk methods

        /// <summary>
        ///  Reads format chunk from BinaryReader file.
        /// </summary>
        /// <returns> instance of FmtChunk that represents format chunk from file </returns>
        /// <exception cref="Exception"> If any error occures </exception>
        private FmtChunk ReadFmtChunk()
        {
            // first 4 bytes - chunk leghth
            byte[] buffer = new byte[4];
            file.Read(buffer, 0, 4);
            uint chlength = BitConverter.ToUInt32(buffer, 0);

            // now reading fmt chunk data
            // format tag
            file.Read(buffer, 0, 2);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            ushort formatTag = BitConverter.ToUInt16(buffer, 0);
            if (formatTag != 1)
                throw new InvalidFileFormatException("No PCM compression format");

            // channels
            file.Read(buffer, 0, 2);
            ushort channels = BitConverter.ToUInt16(buffer, 0);

            // sample rate
            file.Read(buffer, 0, 4);
            uint sampleRate = BitConverter.ToUInt32(buffer, 0);

            // average bytes per second ???
            file.Read(buffer, 0, 4);
            uint avgBytesPerSec = BitConverter.ToUInt32(buffer, 0);

            //sample frame size in bytes
            file.Read(buffer, 0, 2);
            ushort frameSize = BitConverter.ToUInt16(buffer, 0);

            //bit depth, sample size
            file.Read(buffer, 0, 2);
            ushort bitsPerSample = BitConverter.ToUInt16(buffer, 0);
            FmtChunk formatChunk = new FmtChunk(chlength, formatTag, channels, sampleRate, avgBytesPerSec, frameSize, bitsPerSample);
            return formatChunk;
        }

        /// <summary>
        /// Method reads Data chunk from BinaryReader file
        /// </summary>
        /// <returns> Instance of DataChunk holding data from file </returns>
        /// <exception cref="Exception"> If any error occures </exception>
        private DataChunk ReadDataChunk(short bitsPerSample)
        {
            // chunk length
            byte[] buffer = new byte[4];
            file.Read(buffer, 0, 4);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            uint chlength = BitConverter.ToUInt32(buffer, 0);

            // data
            buffer = new byte[chlength];
            //file.Read(buffer, 0, (int)chlength);

            // spliting reads to 
            uint tenPercentsLenght = chlength / 10;
            for (int i = 0; i < 9; i++)
            {
                file.Read(buffer, (int) (i * tenPercentsLenght), (int)tenPercentsLenght);
                IoOperationProgressEventArgs progressArgs = new IoOperationProgressEventArgs(i * 10);
                if (this.ReportProgress != null)
                    ReportProgress(this, progressArgs);
            }

            // read remaining data 
            file.Read(buffer, (int) (9 * tenPercentsLenght) , (int)tenPercentsLenght % 10);
            IoOperationProgressEventArgs progressArg = new IoOperationProgressEventArgs(100);
            if (this.ReportProgress != null)
                ReportProgress(this, progressArg);

            DataChunk dataChunk = new DataChunk(ToDouble(buffer,bitsPerSample));
            return dataChunk;
        }

        #endregion

        #region skip_chunk
        // skiping chunk in the input file

        /// <summary>
        ///  Skips chunk in BinaryReader file. It just moves BinaryReader froward.
        /// </summary>
        /// <exception cref="Exception"> If any error occures </exception>
        private void SkipChunk()
        {
            try
            {
                byte[] buffer = new byte[4];
                // read chunk length
                file.Read(buffer, 0, 4);
                uint chlength = BitConverter.ToUInt32(buffer, 0);
                
                // set new position
                file.BaseStream.Position = file.BaseStream.Position + chlength;
            }
            catch (Exception e)
            {
                throw new Exception(e.Message, e);
            }
        }

        #endregion

        #region convert_to_double
        /* converting ray byte data to daouble from interval [-1;1].
         * For speed improvement every bit depth has different method */

        /// <summary>
        ///  Converts raw byte data to double samples from [-1;1] interval.
        /// </summary>
        /// <param name="data"> Raw byte data in Little endian. </param>
        /// <param name="bitDepth"> Bit depth of the data. </param>
        /// <returns> Samples as doubles </returns>
        private static double[] ToDouble(byte[] data, short bitDepth)
        {
            if (bitDepth == 8)
                return ToDouble8bit(data);

            if (bitDepth == 16)
                return ToDouble16bit(data);

            if (bitDepth == 24)
                return ToDouble24bit(data);

            if (bitDepth == 32)
                return ToDouble32bit(data);

            return null;
        }

        /// <summary>
        ///  Converts raw byte data to double samples from [-1;1] interval using bit depth 8.
        /// </summary>
        /// <param name="data"> Raw byte data in Little endian. </param>
        /// <returns> Samples as doubles </returns>
        private static double[] ToDouble8bit(byte[] data)
        {
            double[] ret = new double[data.Length];

            double maxValue = 1F / 128F;
            for (int i = 0; i < data.Length; i++)
            {
                ret[i] = (data[i] - 128) * maxValue;
            }

            return ret;
        }

        /// <summary>
        ///  Converts raw byte data to double samples from [-1;1] interval using bit depth 16.
        /// </summary>
        /// <param name="data"> Raw byte data in Little endian. </param>
        /// <returns> Samples as doubles </returns>
        private static double[] ToDouble16bit(byte[] data)
        {
            double[] ret = new double[data.Length/2];

            double maxValue = 1F / Math.Pow(2,15);

            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 2)
                {
                    ret[i] = BitConverter.ToInt16(data,source_index) * maxValue;
                }
            }
            else
            {

                byte[] buffer = new byte[2];
                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 2)
                {
                    buffer[0] = data[source_index+1];
                    buffer[1] = data[source_index];

                    ret[i] = BitConverter.ToInt16(buffer, 0) * maxValue;
                }
            }

            return ret;
        }


        /// <summary>
        ///  Converts raw byte data to double samples from [-1;1] interval using bit depth 24.
        /// </summary>
        /// <param name="data"> Raw byte data in Little endian. </param>
        /// <returns> Samples as doubles </returns>
        private static double[] ToDouble24bit(byte[] data)
        {
            double[] ret = new double[data.Length / 3];

            double maxValue = 1F / Math.Pow(2, 23);
            byte[] buffer = new byte[4];

            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 3)
                {
                    buffer[0] = data[source_index];
                    buffer[1] = data[source_index + 1];
                    buffer[2] = data[source_index + 2];
                    if (buffer[2] > 128)
                        buffer[3] = 255;
                    else
                        buffer[3] = 0;

                    ret[i] = BitConverter.ToInt32(buffer, 0) * maxValue;
                }
            }
            else
            {

                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 3)
                {
                    buffer[1] = data[source_index + 2];
                    buffer[2] = data[source_index + 1];
                    buffer[3] = data[source_index];
                    if (buffer[1] > 128)
                        buffer[0] = 255;
                    else
                        buffer[0] = 0;

                    ret[i] = BitConverter.ToInt32(buffer, 0) * maxValue;
                }
            }

            return ret;
        }

        /// <summary>
        ///  Converts raw byte data to double samples from [-1;1] interval using bit depth 32.
        /// </summary>
        /// <param name="data"> Raw byte data in Little endian. </param>
        /// <returns> Samples as doubles </returns>
        private static double[] ToDouble32bit(byte[] data)
        {
            double[] ret = new double[data.Length / 4];

            double maxValue = 1F / Math.Pow(2, 31);

            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 4)
                    ret[i] = BitConverter.ToInt32(data, source_index) * maxValue;
            }
            else
            {
                byte[] buffer = new byte[4];

                for (int i = 0, source_index = 0; i < ret.Length; i++, source_index += 4)
                {
                    buffer[0] = data[source_index + 3];
                    buffer[1] = data[source_index + 2];
                    buffer[2] = data[source_index + 1];
                    buffer[3] = data[source_index];

                    ret[i] = BitConverter.ToInt32(buffer, 0) * maxValue;
                }
            }

            return ret;
        }

        #endregion
    }
}